package cz.pecinovsky.česky.oopnz.správce;
/*******************************************************************************
 *<pre>
 * Předchozí: Není__Toto_je_nově_definovaná_třída
 *
 * Přidáno:
 *    *
 * Upraveno:
 *    *
 *</pre>
 */


////////////////////////////////////////////////////////////////////////////////
//%%+  +++++ Konec ignorovaného počátečního textu +++++

//%A+ >=133x
import cz.pecinovsky.česky.oopnz.utility.Barva;
//%A-
//%A+ >135
import cz.pecinovsky.česky.oopnz.utility.Hodiny;
import cz.pecinovsky.česky.oopnz.utility.IČasovaný;
//%A-
//%A+ >=133x
import cz.pecinovsky.česky.oopnz.utility.IO;
import cz.pecinovsky.česky.oopnz.utility.Pozice;
import cz.pecinovsky.česky.oopnz.utility.Rozměr;
//%A-

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Toolkit;

import java.awt.event.KeyListener;
import java.awt.event.MouseListener;

import java.awt.image.BufferedImage;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

import java.util.ArrayList;
//%I+ @IPřizpůsobivý >135
//import java.util.Collection;
//%I-
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Properties;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.imageio.ImageIO;

import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;



/*******************************************************************************
 * Třída <b><code>SprávcePlátna</code></b> slouží k jednoduchému kreslení
 * na virtuální plátno a připadné následné animaci nakreslených obrázků.
 * </p><p>
 * Třída neposkytuje veřejný konstruktor, protože chce, aby její instance
 * byla jedináček, tj. aby se všechno kreslilo na jedno a to samé plátno.
 * Jediným způsobem, jak získat odkaz na instanci třídy
 * <code>SprávcePlátna</code>,
 * je volaní její statické metody <code>getInstance()</code>.
 * </p><p>
 * Třída <code>SprávcePlátna</code> funguje jako manažer, který dohlíží
 * na to, aby se po změně zobrazení některého z tvarů všechny ostatní tvary
 * řádně překreslily, aby byly správně zachyceny všechny překryvy
 * a aby se při pohybu jednotlivé obrazce vzájemně neodmazavaly.
 * Aby vše správně fungovalo, je možno použít jeden dvou přístupů:</p>
 * <ul>
 *   <li>Manažer bude obsah plátna překreslovat
 *       <b>v pravidelných intervalech</b>
 *       bez ohledu na to, jestli se na něm udála nějaká změna či ne.
 *       <ul><li>
 *       <b>Výhodou</b> tohoto přístupu je, že se zobrazované objekty
 *       nemusí starat o to, aby se manažer dozvěděl, že se jejich stav změnil.
 *       </li><li>
 *       <b>Neýhodou</b> tohoto přístupu je naopak to, že manažer
 *       spotřebovává na neustálé překreslování jistou část výkonu
 *       procesoru, což může u pomalejších počítačů působit problémy.
 *       <br>&nbsp;</li></ul></li>
 *   <li>Manažer překresluje plátno <b>pouze na výslovné požádání</b>.
 *       <ul><li>
 *       <b>Výhodou</b> tohoto přístupu je úspora spotřebovaného výkonu
 *       počítače v období, kdy se na plátně nic neděje.
 *       </li><li>
 *       <b>Nevýhodou</b> tohoto přístupu je naopak to, že kreslené
 *       objekty musí na každou změnu svého stavu upozornit manažera,
 *       aby věděl, žed má plátno překreslit.
 *   </li>
 * </ul><p>
 * Třída <code>SprávcePlátna</code> požívá druhou z uvedených strategií,
 * tj. <b>překresluje plátno pouze na požádání</b>.
 * </p><p>
 * Obrazec, který chce být zobrazován na plátně, se musí nejprve přihlásit
 * u instance třídy <code>SprávcePlátna</code>, aby jej tato zařadila
 * mezi spravované obrazce (sada metod <code>přidej&hellip;</code>).
 * Přihlásit se však mohou pouze instance tříd, které implementují
 * rozhraní <code>IKreslený</code>.
 * </p><p>
 * Nepřihlášený obrazec nemá šanci býti zobrazen, protože na plátno
 * se může zobrazit pouze za pomoci kreslítka, jež může získat jedině od
 * instance třídy <code>SprávcePlátna</code>, ale ta je poskytuje pouze
 * instancím, které se přihlásily do její správy.
 * </p><p>
 * Obrazec, který již dále nemá byt kreslen, se muže odhlásit zavoláním
 * metody <code>odstraň(IKreslený)</code>.Zavoláním metody
 * <code>odstraňVše()</code> se ze seznamu spravovaných (a tím i z plátna)
 * odstraní všechny vykreslované obrazce.
 * </p><p>
 * Efektivitu vykreslování je možné ovlivnit voláním metody
 * <code>nekresli()</code>, která pozastaví překreslování plátna po nahlášených
 * změnách. Její volání je výhodné např. v situaci, kdy je třeba vykreslit
 * obrazec složený z řady menších obrazců a bylo by nevhodné překreslovat
 * plátno po vykreslení každého z nich.
 * </p><p>
 * Do původního stavu převedeme plátno voláním metody <code>vraťKresli()</code>,
 * která vrátí vykreslování do stavu před posledním voláním metody
 * <code>nekresli()</code>. Nemůžeč se tedy stát, že by se při zavolání metody
 * <code>nekresli()</code> v situaci, kdy je již vykreslování pozastaveno,
 * začalo po následém zavolání <code>vraťKresli()</code> hned vykreslovat.
 * Po dvou voláních <code>vraťKresli()</code> se začne vykreslovat až po
 * dvou zavoláních <code>vraťKresli()</code>.
 * </p><p>
 * Proto plátno pouze žádáme, aby se vrátilo do toho kreslícího stavu,
 * ve kterém bylo v okamžiku, kdy jsme je naposledy žádali o to,
 * aby se přestalo překreslovat. Nemůže se tedy stát, že by se při zavolání
 * metody <code>nekresli()</code> v situaci, kdy je již vykreslování
 * pozastaveno, začalo po následném zavolání <code>vraťKresli()</code> hned
 * vykreslovat.
 * </p><p>
 * Každé zavolání metody <code>nekresli()</code> musí být doplněno
 * odpovídajícím voláním <code>vraťKresli()</code>. Teprve když poslední
 * <code>vraťKresli()</code> odvolá první <code>nekresli()</code>, bude
 * překreslování opět obnoveno.
 * </p>
 *
 * @author Rudolf PECINOVSKÝ
 * @version 6.04.2017 — 2011-02-04
 */
public final class SprávcePlátna
{
//    static { System.out.println("Inicializace třídy SprávcePlátna"); }
//    { System.out.println("Inicializace instance třídy SprávcePlátna"); }
//== KONSTANTNÍ ATRIBUTY TŘÍDY =================================================

    /** Titulek okna aktivního plátna. */
    private static final String TITULEK_0  = "Plátno ovládané správcem";

    /** Počáteční políčková šířka aktivní plochy plátna. */
    private static final int ŠÍŘKA_0 = 6;

    /** Počáteční políčková výška aktivní plochy plátna. */
    private static final int VÝŠKA_0 = 6;

    /** Počáteční barva pozadí plátna. */
    private static final Barva POZADÍ_0 = Barva.KRÉMOVÁ;

    /** Počáteční barva čar mřížky. */
    private static final Barva BARVA_ČAR_0 = Barva.ČERNÁ;

    /** Implicitní rozteč čtvercové sítě. */
    private static final int KROK_0 = 50;

    /** Maximální povolená velikost rozteče čtvercové sítě. */
    private static final int MAX_KROK = 200;

    /** Jediná instance třídy SprávcePlátna. */
    private static final SprávcePlátna SP;

    /** Zámek zabezepčující plynulou funkci grafiky. */
    private static final Lock ZÁMEK = new ReentrantLock();



//== PROMĚNNÉ ATRIBUTY TŘÍDY ===================================================
//== STATICKÝ INICIALIZAČNÍ BLOK - STATICKÝ KONSTRUKTOR ========================

    static
    {
        SP = new SprávcePlátna();

        //Při kreslení čar se ptá APosuvný po správci plánta. Proto se mohou
        //čáry kreslit (a tím i volat metoda setKrokRozměr(int,int,int))
        //až poté, co bude jednináček (konstanta SP) inicializován.

        Point pozice = konfiguraceZeSouboru();

        ZÁMEK.lock();
        try
        {
//%I+ @DB_SP
//          System.out.println("Správce vytvořen");
//%I-
            //Připraví a vykreslí prázdné plátno (souřadnice jsou již načteny)
            SP.setPozice(pozice.x, pozice.y);
//%I+ @DB_SP
//            System.out.println("Pozice nastavena");
//%I-
            SP.setKrokRozměr(KROK_0, ŠÍŘKA_0, VÝŠKA_0);
//%I+ @DB_SP
//            System.out.println("Krok a rozměr nastaven");
//%I-
            //Nyní je znám rozměr plátna, tak můžeme umístit dialogová okna
            int x = SP.okno.getX();
            int y = SP.okno.getY() + SP.okno.getHeight();
            IO.oknaNa(x, y);
//%I+ @DB_SP
//            System.out.println("IO nastaveno");
//%I-
            SP.setNázev(TITULEK_0);
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }



//== KONSTANTNÍ ATRIBUTY INSTANCÍ ==============================================

    /** Aplikační okno animačního plátna. */
    private final JFrame okno;

    /** Instance lokální třídy, která je zřízena proto, aby odstínila
     *  metody svého rodiče JPanel. */
    private final JPanel plátno;

    /** Seznam zobrazovaných předmětů. */
    private final List<IKreslený> předměty = new ArrayList<IKreslený>();
//%I+ @IPřizpůsobivý >135
//
//    /** Seznam přihlášených přizpůsobivých objektů. */
//    private final Collection<IPřizpůsobivý> přizpůsobiví =
//                                            new ArrayList<IPřizpůsobivý>();
//%I-



//== PROMĚNNÉ ATRIBUTY INSTANCÍ ================================================

    //Z venku neovlivnitelné Atributy pro zobrazeni plátna v aplikačním okně

        /** Vše se kreslí na obraz - ten se snadněji překreslí. */
        private Image obrazPlátna;

        /** Kreslítko získané od obrazu plátna, na nějž se vlastně kreslí. */
        private Kreslítko kreslítko;

        /** Semafor bránící příliš častému překreslování. Překresluje se pouze
         *  je-li ==0. Nesmi byt <0. */
        private int nekreslit = 0;

        /** Příznak toho, že kreslení právě probíhá,
         *  takže vypínání nefunguje. */
        private boolean kreslím = false;

        /** Čáry zobrazující na plántě mřížku. */
        private Čára[] vodorovná,   svislá;

    //Přímo ovlivnitelné atributy

        /** Rozteč čtvercové sítě. */
        private int krok = KROK_0;

        /** Zobrazuje-li se mřížka. */
        private boolean mřížka = true;

        /** Barva pozadí plátna. */
        private Barva barvaPozadí = POZADÍ_0;

        /** Barva čar mřížky. */
        private Barva barvaČar = BARVA_ČAR_0;

        /** Šířka aktivní plochy plátna v udávaná v polích. */
        private int sloupců = ŠÍŘKA_0;

        /** Výška aktivní plochy plátna v udávaná v polích. */
        private int řádků = VÝŠKA_0;

        /** Šířka aktivní plochy plátna v bodech. */
        private int šířkaBodů = ŠÍŘKA_0 * krok;

        /** Výška aktivní plochy plátna v bodech. */
        private int výškaBodů = VÝŠKA_0 * krok;
//%A+ >135

        /** Zda se mají přizpůsobiví upozorňovat na změny rozměru pole. */
        private boolean hlásitZměnyRozměru = true;
//%A-

        /** Zda je možno měnit velikost kroku. */
        private Object vlastníkPovoleníZměnyKroku = null;

        /** Pozice plátna na obrazovace - při používání více obrazovek
         *  je občas třeba ji po zviditelnění obnovit. */
        Point pozice;



//== PŘÍSTUPOVÉ METODY VLASTNOSTÍ TŘÍDY ========================================
//== OSTATNÍ NESOUKROMÉ METODY TŘÍDY ===========================================

//##############################################################################
//== KONSTRUKTORY A TOVÁRNÍ METODY =============================================

    /***************************************************************************
     * Jediná metoda umožnující získat odkaz na instanci plátna.
     * Vrací vsak pokaždé odkaz na stejnou instanci,
     * protože instance plátna je jedináček.
     * <p>
     * Pokud instance při volaní metody ještě neexistuje,
     * metoda instanci vytvoří.</p>
     *
     * @return Instance třídy {@code SprávcePlátna}
     */
    public static SprávcePlátna getInstance()
    {
        return SP;
    }


    /***************************************************************************
     * Vytvoří instanci třídy - jedináčka => je volán pouze jednou.
     */
    @SuppressWarnings("serial")
    private SprávcePlátna()
    {
        okno = připravOkno();

//%I+ @DB_SP
//        System.out.println("Okno připraveno");
//%I-
        //Vlastní plátno je definováno jako instance anonymní třídy
        plátno =
            new JPanel()
            {   /** Povinně překrývaná abstraktní metoda třídy JPanel. */
                @Override
                public void paintComponent(Graphics g)
                {
                    if(!ZÁMEK.tryLock())
                    {
                        try
                        {
                            ZÁMEK.notifyAll();
                        }
                        catch(java.lang.IllegalMonitorStateException Ex)
                        {
                            //Stalo se neco divneho, skoncim tento pokus
                            //o prekresleni a zkusim to znova;
                            Runnable updateAComponent = new Runnable() {
                                @Override
                                public void run() { plátno.repaint(); }
                            };
                            SwingUtilities.invokeLater(updateAComponent);
                            return;
                        }
                    }
                    try
                    {
                        g.drawImage(obrazPlátna, 0, 0, null);
                    }
                    finally
                    {
                        ZÁMEK.unlock();
                    }
                }
            };//Konec definice třídy plátna
//%I+ @DB_SP
//        System.out.println("Plátno vytvořeno");
//%I-
        okno.setContentPane(plátno);
//%I+ @DB_SP
//        System.out.println("Plátno přidáno");
//%I-
    }



//== ABSTRAKTNÍ METODY =========================================================
//== PŘÍSTUPOVÉ METODY VLASTNOSTÍ INSTANCÍ =====================================

    /***************************************************************************
     * Nastaví rozměr plátna zadáním bodové velikosti políčka a
     * počtu políček ve vodorovném a svislém směru.
     * Při velikosti políčka = 1 se vypíná zobrazování mřížky.
     *
     * @param  krok    Nová bodová velikost políčka
     * @param  pŠířka  Nový počet políček vodorovně
     * @param  pVýška  Nový počet políček svisle
     */
    public void setKrokRozměr(int krok, int pŠířka, int pVýška)
    {
        ZÁMEK.lock();
        try
        {
            setKrokRozměr(krok, pŠířka, pVýška, null);
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }


    /***************************************************************************
     * Nastaví rozměr plátna zadáním bodové velikosti políčka a
     * počtu políček ve vodorovném a svislém směru.
     * Při velikosti políčka = 1 se vypíná zobrazování mřížky.
     *
     * @param  krok    Nová bodová velikost políčka
     * @param  pŠířka  Nový počet políček vodorovně
     * @param  pVýška  Nový počet políček svisle
     * @param  měnič   Objekt, který žádá o změnu rozměru. Jakmile je jednou
     *                 tento objekt nastaven, nesmí již rozměr plátna
     *                 měnit nikdo jiný.
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    void setKrokRozměr(int krok, int pŠířka, int pVýška, Object měnič)
    {
//%I+ @DB_SP
//        System.out.println("setKrokRozmer - vstup");
//%I-
        ZÁMEK.lock();
        try
        {
            //Kontrola, jestli rozměry mění ten, kdo je měnit smí
            if ((měnič != null)  &&
                (měnič != vlastníkPovoleníZměnyKroku))
            {
                if (vlastníkPovoleníZměnyKroku == null) {
                    vlastníkPovoleníZměnyKroku = měnič;
                } else {
                    throw new IllegalStateException(
                        "Změna kroku a rozměru není danému objektu povolena");
                }
            }
            //Ověření korektnosti zadaných parametrů
            Dimension obrazovka = Toolkit.getDefaultToolkit().getScreenSize();
            if ((krok   < 1)  ||
                (pŠířka < 2)  ||  (obrazovka.width  < šířkaBodů) ||
                (pVýška < 2)  ||  (obrazovka.height < výškaBodů))
            {
                throw new IllegalArgumentException(
                    "\nŠpatně zadané rozměry: " +
                    "\n  krok =" + krok  + " bodů," +
                    "\n  šířka=" + pŠířka + " polí = " + pŠířka*krok + " bodů," +
                    "\n  výška=" + pVýška + " polí = " + pVýška*krok + " bodů," +
                    "\n  obrazovka= " + obrazovka.width  + "×" +
                                        obrazovka.height + " bodů\n");
            }

            šířkaBodů = pŠířka * krok;
            výškaBodů = pVýška * krok;

            okno.setResizable(true);
            plátno.setPreferredSize(new Dimension(šířkaBodů, výškaBodů));
            okno.pack();
            okno.setResizable(false);

            obrazPlátna = plátno.createImage(šířkaBodů, výškaBodů);
            kreslítko   = new Kreslítko((Graphics2D)obrazPlátna.getGraphics());
            kreslítko.setPozadí(barvaPozadí);

            int starý    = this.krok;
            this.krok    = krok;
            this.sloupců = pŠířka;
            this.řádků   = pVýška;

            připravČáry();

//%A+ >135
            if (hlásitZměnyRozměru  &&  (krok != starý))
            {
                nekreslit++; {
//%A-
//%A+ @IPřizpůsobivý >135
//                    for (IPřizpůsobivý ip : přizpůsobiví) {
//                        ip.krokZměněn(starý, krok);
//                    }
//%A-
//%A+ >135
                    for (IKreslený ik : seznamKreslených()) {
                        if (ik instanceof IPřizpůsobivý) {
                            ((IPřizpůsobivý)ik).krokZměněn(starý, krok);
                        }
                    }
                }nekreslit--;
            }
//%A-
            IO.Oprava.rozměrOkna(okno);
            IO.Oprava.poziceOkna(okno);
//%I+ @DB_SP
//            System.out.println("setKrokRozmer - pred nastavenim viditelnosti");
//%I-
            setViditelné(true);
            překresli();
//%I+ @DB_SP
//            System.out.println("setKrokRozmer - po nastaveni viditelnosti");
//            překresli();  //Překresluje již předchozí metoda
//            System.out.println("setKrokRozmer - po prekresleni");
//%I-
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }


    /***************************************************************************
     * Vrátí vzdálenost čar mřížky = bodovou velikost políčka.
     *
     * @return Bodová velikost políčka
     */
     public int getKrok()
     {
         return krok;
     }


    /***************************************************************************
     * Nastaví vzdálenost čar mřížky = bodovou velikost políčka.
     * Při velikosti políčka = 1 se vypíná zobrazování mřížky.
     *
     * @param velikost  Nová bodová velikost políčka
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    void setKrok(int velikost)
    {
        ZÁMEK.lock();
        try
        {
            setKrokRozměr(velikost, sloupců, řádků);
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }


    /***************************************************************************
     * Vrátí počet sloupců plátna, tj. jeho políčkovou šířku.
     *
     * @return  Aktuální políčková šířka plátna (počet políček vodorovně)
     */
    public int getSloupců()
    {
        return sloupců;
    }


    /***************************************************************************
     * Vrátí bodovou šířku plátna.
     *
     * @return  Aktuální bodová šířka plátna (počet bodů vodorovně)
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    int getBšířka()
    {
        return šířkaBodů;
    }


    /***************************************************************************
     * Vrátí počet řádků plátna, tj. jeho políčkovou výšku.
     *
     * @return  Aktuální políčková výška plátna (počet políček svisle)
     */
    public int getŘádků()
    {
        return řádků;
    }


    /***************************************************************************
     * Vrátí bodovou výšku plátna.
     *
     * @return  Aktuální bodová výška plátna (počet bodů svisle)
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    int getBVýška()
    {
        return výškaBodů;
    }

//%A+ >135

    /***************************************************************************
     * Vrátí políčkový rozměr plátna, tj. šířku a výšku v polích.
     *
     * @return  Aktuální políčkový rozměr plátna
     */
    public Rozměr getRozměr()
    {
        return new Rozměr(sloupců, řádků);
    }

//%A-

    /***************************************************************************
     * Nastaví rozměr plátna zadáním jeho políčkové výsky a šířky.
     *
     * @param  sloupců  Nový počet políček vodorovně
     * @param  řádků    Nový počet políček svisle
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    void setRozměr(int sloupců, int řádků)
    {
        ZÁMEK.lock();
        try
        {
            setKrokRozměr(krok, sloupců, řádků);
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }

//%A+ >135

    /***************************************************************************
     * Nastaví rozměr plátna zadáním jeho políčkové výsky a šířky.
     *
     * @param  rozměr  Zadávaný rozměr v počtu políček
     */
    public void setRozměr(Rozměr rozměr)
    {
        ZÁMEK.lock();
        try
        {
            setRozměr(rozměr.šířka, rozměr.výška);
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }

//%A-

    /***************************************************************************
     * Vrátí informaci o tom, je-li zobrazována mřížka.
     *
     * @return Mřížka je zobrazována = true, není zobrazována = false.
     */
    public boolean isMřížka()
    {
    	return mřížka;
    }


    /***************************************************************************
     * V závislosti na hodntě parametru nastaví nebo potlačí
     * zobrazování čar mřížky.
     *
     * @param zobrazit  Jestli mřížku zobrazovat.
     */
    public void setMřížka(boolean zobrazit)
    {
        ZÁMEK.lock();
        try
        {
            mřížka = zobrazit;
            připravČáry();
            překresli();
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }


    /***************************************************************************
     * Poskytuje informaci o aktuální viditelnosti okna.
     *
     * @return Je-li okno viditelné, vrací <b>true</b>, jinak vrací <b>false</b>
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    boolean isViditelné()
    {
        return okno.isVisible();
    }


    /***************************************************************************
     * V závislosti na hodnotě svého parametru
     * nastaví nebo potlačí viditelnost plátna na displeji.
     *
     * @param viditelné logická hodnota požadované viditelnost (true=viditelné)
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    void setViditelné(boolean viditelné)
    {
        boolean překresleno = false;
        boolean změna = (isViditelné() != viditelné);
        if (změna)
        {
            pozice = okno.getLocation();
//%I+ @DB_SP
//            System.out.println("Před okno.setVisible(viditelné)");
//%I-
            okno.setVisible(viditelné);
            if (viditelné)
            {
                //Na WinXP při více obrazovkách po zviditelnění blblo =>
                //bylo třeba znovu nastavit pozici
                okno.setLocation(pozice);

                okno.setAlwaysOnTop(true);
                okno.toFront();
                překresli();
                okno.setAlwaysOnTop(false);
            }
//%I+ @DB_SP
//            System.out.println("Za okno.setVisible(viditelné)");
//%I-
        }
    }


    /***************************************************************************
     * Vrátí aktuální barvu pozadí.
     *
     * @return  Nastavena barva pozadí
     */
    public Barva getBarvaPozadí()
    {
        return barvaPozadí;
    }


    /***************************************************************************
     * Nastaví pro plátno barvu pozadí.
     *
     * @param  barva  Nastavovaná barva pozadí
     */
    public void setBarvaPozadí(Barva barva)
    {
        barvaPozadí = barva;
        kreslítko.setPozadí(barvaPozadí);
        překresli();
    }


    /***************************************************************************
     * Pomocná metoda pro účely ladění aby bylo možno zkontrolovat,
     * ze na konci metody má semafor stejnou hodnotu, jako měl na počátku.
     *
     * @return  Stav vnitřního semaforu: >0  - nebude se kreslit,<br>
     *                                   ==0 - kreslí se,<br>
     *                                   <0  - chyba
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    int getNekresli()
    {
        return nekreslit;
    }


    /***************************************************************************
     * Vrátí aktuální název v titulkové liště okna plátna.
     *
     * @return  Aktuální název okna
     */
    public String getNázev()
    {
        return okno.getTitle();
    }


    /***************************************************************************
     * Nastaví název v titulkové liště okna plátna.
     *
     * @param název  Nastavovaný název
     */
    public void setNázev(String název)
    {
        okno.setTitle(název);
    }

//%A+ >135

    /***************************************************************************
     * Vrátí vodorovnou souřadnici aplikačního okna plátna.
     *
     * @return Pozice levého horního rohu aplikačního okna plátna.
     */
    public Pozice getPozice()
    {
        return new Pozice(okno.getX(), okno.getY());
    }

//%A-

    /***************************************************************************
     * Nastaví pozici aplikačního okna aktivního plátna na obrazovce.
     *
     * @param x  Vodorovná souřadnice aplikačního okna plátna.
     * @param y  Svislá souřadnice aplikačního okna plátna.
     */
    public void setPozice(int x, int y)
    {
        okno.setLocation(x, y);
        pozice = new Point(x, y);
    }

//%A+ >135

    /***************************************************************************
     * Nastaví pozici aplikačního okna aktivního plátna na obrazovce.
     *
     * @param pozice  Požadovaná pozice aplikačního okna plátna na obrazovce.
     */
    public void setPozice(Pozice pozice)
    {
        okno.setLocation(pozice.getX(), pozice.getY());
    }


    /***************************************************************************
     * Vrátí instanci třídy <code>Obrázek</code> zobrazující zadaný výřez
     * AktivníhoPlátna.
     * @param x     Vodorovná pozice požadovaného výřezu
     * @param y     Svislá pozice požadovaného výřezu
     * @param šířka Šířka požadovaného výřezu v bodech
     * @param výška Výška požadovaného výřezu v bodech
     * @return Instance třídy <code>Obrázek</code> zobrazující zadaný výřez
     */
    public Obrázek getObrázek(int x, int y, int šířka, int výška)
    {
        BufferedImage bim = getBufferedImage(x, y, šířka, výška);
        return new Obrázek(0, 0, bim);
    }

//%A-


//== OSTATNÍ NESOUKROMÉ METODY INSTANCÍ ========================================

    /***************************************************************************
     * Převede instanci na řetězec. Používá se především při ladění.
     *
     * @return Řetězcová reprezentace dané instance.
     */
    @Override
    public String toString()
    {
        return getClass().getName() + "(krok=" + krok +
                ", šířka=" + sloupců + ", výška=" + řádků +
                ", pozadí=" + barvaPozadí + ")";
    }


    /***************************************************************************
     * Vykreslí všechny elementy.
     */
    public void překresli()
    {
        ZÁMEK.lock();
        try
        {
            if (kreslím) {    //Právě překresluji - volám nepřímo sám sebe
                return;
            }
            if ((nekreslit == 0)  &&  isViditelné())   //Mám kreslit a je proč
            {
                kreslím = true;
                kreslítko.vyplňRám(0, 0, šířkaBodů, výškaBodů,
                                    barvaPozadí);
                if (mřížka  &&  (barvaČar != barvaPozadí))
                {
                    //Budeme kreslit mřížku -- bude pod obrazci
                    for (int i=0;   i < sloupců;  ) {
                        svislá[i++].nakresli(kreslítko);
                    }
                    for (int i=0;   i < řádků;  ) {
                        vodorovná[i++].nakresli(kreslítko);
                    }
                }
                for (IKreslený předmět : předměty) {
                    předmět.nakresli(kreslítko);
                }

                //Aktualizované plátno se překreslí
                Runnable updateAComponent = new Runnable() {
                    @Override
                    public void run() { plátno.repaint(); }
                };
                SwingUtilities.invokeLater(updateAComponent);

                kreslím = false;        //Už nekreslím
            }
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }


    /***************************************************************************
     * Potlačí překreslování plátna, přesněji zvýší hladinu potlačení
     * překreslování o jedničku. Návratu do stavu před voláním této metody
     * se dosáhne zavoláním metody <code>vraťKresli()</code>.</p>
     * <p>
     * Metody <code>nekresli()</code> a <code>vraťKresli()</code>
     * se tak chovají obdobně jako závorky, mezi nimiž je vykreslování
     * potlačeno.</p>
     */
    public void nekresli()
    {
        ZÁMEK.lock();
        try
        {
            nekreslit++;
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }


    /***************************************************************************
     * Vrátí překreslování do stavu před posledním voláním metody
     * <code>nekresli()</code>. Předcházelo-li proto více volání metody
     * <code>nekresli()</code>, začne se překreslovat až po odpovídajím počtu
     * zavolání metody <code>vraťKresli()</code>.
     *
     * @throws IllegalStateException
     *         Je-li metoda volána aniž by předcházelo odpovídající volání
     *         <code>nekresli()</code>.
     */
    public void vraťKresli()
    {
        ZÁMEK.lock();
        try
        {
            if (nekreslit == 0) {
                throw new IllegalStateException(
                    "Vrácení do stavu kreslení musí přecházet zákaz!");
            }
            nekreslit--;
            if (nekreslit == 0)  {
                překresli();
            }
        }
        catch(IllegalStateException ex)
        {
            throw ex;
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }

//%A+ >135

    /***************************************************************************
     * Odpojí se od hodin a přestává synchronizovat
     * své překreslování s jejich tikotem.
     */
    public void vypniHodiny()
    {
        Hodiny hodiny = Hodiny.getInstance();
        hodiny.setÚvod (null);
        hodiny.setZávěr(null);
    }


    /***************************************************************************
     * Napojí se na hodiny, aby se na začátku tiku vypnulo kreslení
     * a na konci tiku opět zapnulo.
     */
    public void zapniHodiny()
    {
        Hodiny hodiny = Hodiny.getInstance();
        hodiny.setÚvod(new IČasovaný() {
            @Override
            public void tik() { nekresli(); }
        });
        hodiny.setZávěr(new IČasovaný() {
            @Override
            public void tik() { vraťKresli(); }
        });
    }

//%A-

    /***************************************************************************
     * Odstraní zadaný obrazec ze seznamu malovaných.
     * Byl-li obrazec v seznamu, překreslí plátno.
     *
     * @param obrazec  Odstraňovaný obrazec
     *
     * @return  true v případě, když obrazec v seznamu byl,
     *          false v případě, když nebylo co odstraňovat
     */
    public boolean odstraň(IKreslený obrazec)
    {
        ZÁMEK.lock();
        try
        {
            boolean ret = předměty.remove(obrazec);
            if (ret) {
                překresli();
            }
            return ret;
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }


    /***************************************************************************
     * Vyčisti plátno, tj. vyprázdní seznam malovaných
     * (odstraní z něj všechny obrazce).
     */
    public void odstraňVše()
    {
        ZÁMEK.lock();
        try
        {
            nekresli(); {
                ListIterator<IKreslený> it = předměty.listIterator();
                while (it.hasNext()) {
                    it.next();
                    it.remove();
                }
            } vraťKresli();
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }


    /***************************************************************************
     * Není-li zadaný obrazec v seznamu malovaných, přidá jej na konec
     * (bude se kreslit jako poslední, tj. na vrchu.
     * Byl-li obrazec opravdu přidán, překreslí plátno.
     * Objekty budou vždy kresleny v pořadí, v němž byly přidány do správy,
     * tj. v seznamu parametrů zleva doprava
     * a dříve zaregistrované objekty před objekty zaregistrovanými později.
     *
     * @param  obrazec  Přidávané obrazce
     * @return  Počet skutečně přidaných obrazců
     */
    public int přidej(IKreslený... obrazec)
    {
        ZÁMEK.lock();
        try
        {
            int počet = 0;
            nekresli(); {
                for (IKreslený ik : obrazec)
                {
                    if (! předměty.contains(ik)) {
                        předměty.add(ik);
                        počet++;
                    }
                }
            } vraťKresli();
            return počet;
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }


    /***************************************************************************
     * Přidá obrazec do seznamu malovaných tak, aby byl kreslen
     * nad zadaným obrazcem.
     * Pokud již v seznamu byl, jenom jej přesune do zadané pozice.
     *
     * @param  současný  Obrazec, který má byt při kreslení pod
     *                    přidávaným obrazcem
     * @param  přidaný   Přidávaný obrazec
     *
     * @return  true  v případě, když byl obrazec opravdu přidán,
     *          false v případě, když již mezi zobrazovanými byl
     *                a pouze se přesunul do jiné urovné
     */
    public boolean přidejNad(IKreslený současný, IKreslený přidaný)
    {
        ZÁMEK.lock();
        try
        {
            boolean nebyl = ! předměty.remove(přidaný);
            int kam = předměty.indexOf(současný);
            if (kam < 0)
            {
                throw new IllegalArgumentException(
                    "Referenční objekt není na plátně zobrazován!");
            }
            předměty.add(kam+1, přidaný);
            překresli();
            return nebyl;
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }


    /***************************************************************************
     * Přidá obrazec do seznamu malovaných tak, aby byl kreslen
     * pod zadaným obrazcem.
     * Pokud již v seznamu byl, jenom jej přesune do zadané pozice.
     *
     * @param  současný  Obrazec, který má byt při kreslení nad
     *                   přidávaným obrazcem
     * @param  přidaný   Přidávaný obrazec
     *
     * @return  true  v případě, když byl obrazec opravdu přidán,
     *          false v případě, když již mezi zobrazovanými byl
     *                a pouze se přesunul do jiné urovné
     */
    public boolean přidejPod(IKreslený současný, IKreslený přidaný)
    {
        ZÁMEK.lock();
        try
        {
            boolean nebyl = ! předměty.remove(přidaný);
            int kam = předměty.indexOf(současný);
            if (kam < 0)
            {
                throw new IllegalArgumentException(
                    "Referenční objekt není na plátně zobrazován!");
            }
            předměty.add(kam, přidaný);
            překresli();
            return nebyl;
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }


    /***************************************************************************
     * Přidá obrazec do seznamu malovaných tak, aby byl kreslen
     * nad všemi obrazci.
     * Pokud již v seznamu byl, jenom jej přesune do požadované pozice.
     *
     * @param  přidaný   Přidávaný obrazec
     *
     * @return  true  v případě, když byl obrazec opravdu přidán,
     *          false v případě, když již mezi zobrazovanými byl
     *                a pouze se přesunul do jiné urovné
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    boolean přidejNavrch(IKreslený přidaný)
    {
        ZÁMEK.lock();
        try
        {
            boolean nebyl = ! předměty.remove(přidaný);
            předměty.add(přidaný);
            překresli();
            return nebyl;
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }


    /***************************************************************************
     * Přidá obrazec do seznamu malovaných tak, aby byl kreslen
     * pod zadaným obrazcem.
     * Pokud již v seznamu byl, jenom jej přesune do zadané pozice.
     *
     * @param  přidaný   Přidávaný obrazec
     *
     * @return  true  v případě, když byl obrazec opravdu přidán,
     *          false v případě, když již mezi zobrazovanými byl
     *                a pouze se přesunul do jiné urovné
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    boolean přidejDospod(IKreslený přidaný)
    {
        ZÁMEK.lock();
        try
        {
            boolean nebyl = ! předměty.remove(přidaný);
            předměty.add(0, přidaný);
            překresli();
            return nebyl;
        }
        finally
        {
            ZÁMEK.unlock();
        }
    }


    /***************************************************************************
     * Vrátí pořadí zadaného prvku v seznamu kreslených prvků.
     * Prvky se přitom kreslí v rostoucím pořadí, takže obrazec
     * s větším poradím je kreslen nad obrazcem s menším poradím.
     * Neni-li zadaný obrazec mezi kreslenými, vrátí -1.
     *
     * @param  obrazec  Objekt, na jehož kreslicí pořadí se dotazujeme
     *
     * @return  Pořadí obrazce; prvý kresleny obrazec má pořadí 0.
     *          Neni-li zadaný obrazec mezi kreslenými, vrátí -1.
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    int pořadí(IKreslený obrazec)
    {
        return předměty.indexOf(obrazec);
    }


    /***************************************************************************
     * Vrátí nemodifikovatelný seznam všech spravovaných obrázků.
     *
     * @return  Požadovaný seznam
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    List<IKreslený> seznamKreslených()
    {
        return Collections.unmodifiableList(předměty);
    }

//%I+ @IPřizpůsobivý >135
//
//    /***************************************************************************
//     * Přihlásí posluchače změny velikosti pole.
//     *
//     * @param  posluchač  Přihlašovaný posluchač
//     * @return  Informace o tom, byl-li posluchač doopravdy přidán -
//     *          false oznamuje, že posluchač už byl přihlášen
//     *          a nebylo jej proto třeba přidávat.
//     */
//    public boolean přihlašPřizpůsobivý(IPřizpůsobivý posluchač)
//    {
//        return přizpůsobiví.add(posluchač);
//    }
//
//
//    /***************************************************************************
//     * Odhlásí posluchače změny velikosti pole.
//     *
//     * @param  posluchač  Odhlašovaný posluchač
//     * @return  Informace o tom, byl-li posluchač doopravdy odebrán -
//     *          false oznamuje, že posluchač už nebyl přihlášen
//     *          a nebylo jej proto třeba odebírat.
//     */
//    public boolean odhlašPřizpůsobivý(IPřizpůsobivý posluchač)
//    {
//        return přizpůsobiví.remove(posluchač);
//    }
//
//%I-
//%A+ >135

    /***************************************************************************
     * Nastaví, zda se mají přihlášeným posluchačům hlásit změny
     * velikosti kroku a vrátí původní nastavení.
     *
     * @param hlásit  Požadované nastavení (true=hlásit, false=nehlasit).
     * @return Původní nastavení
     */
    public boolean hlásitZměnyRozměru(boolean hlásit)
    {
        boolean ret = hlásitZměnyRozměru;
        hlásitZměnyRozměru = hlásit;
        return ret;
    }

//%A-

    /***************************************************************************
     * Přihlásí posluchače událostí klávesnice.
     *
     * @param posluchač  Přihlašovaný posluchač
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    void přihlašKlávesnici(KeyListener posluchač)
    {
        okno.addKeyListener(posluchač);
    }


    /***************************************************************************
     * Odhlásí posluchače klávesnice.
     *
     * @param posluchač  Odhlašovaný posluchač
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    void odhlašKlávesnici(KeyListener posluchač)
    {
        okno.removeKeyListener(posluchač);
    }


    /***************************************************************************
     * Přihlásí posluchače událostí myši.
     *
     * @param posluchač  Přihlašovaný posluchač
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    void přihlašMyš(MouseListener posluchač)
    {
        okno.addMouseListener(posluchač);
    }


    /***************************************************************************
     * Odhlásí posluchače myši.
     *
     * @param posluchač  Odhlašovaný posluchač
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    void odhlašMyš(MouseListener posluchač)
    {
        okno.removeMouseListener(posluchač);
    }


    /***************************************************************************
     * Uloží obraz aktivního plátna do zadaného souboru.
     *
     * @param soubor Soubor, do nějž se má obraz plátna uložit
     */
//%I+ <135
//    private
//%I-
//%A+ >135
    public
//%A-
    void uložJakoObrázek(File soubor)
    {
        BufferedImage bim = getBufferedImage();
        try {
            ImageIO.write(bim, "PNG", soubor);
        } catch(IOException exc)  {
            throw new RuntimeException(
            	"\nObraz aktivního plátna se nepodařilo uložit do souboru " +
                soubor,  exc);
        }
    }


//== SOUKROMÉ A POMOCNÉ METODY TŘÍDY ===========================================

    /***************************************************************************
     * Přečte parametry z konfiguračního souboru.
     * Tento soubor je umístěn v domovském adresáři uživatele
     * ve složce {@code .rup} v souboru {@code bluej.properties}.
     */
    private static Point konfiguraceZeSouboru() {
        Point pozice;
        Properties sysProp = System.getProperties();
        String     userDir = sysProp.getProperty("user.home");
        File       rupFile = new File(userDir, ".rup/bluej.properties");
        Properties rupProp = new Properties();
        try {
            Reader reader = new FileReader(rupFile);
            rupProp.load(reader);
            reader.close();
            String sx = rupProp.getProperty("canvas.x");
            String sy = rupProp.getProperty("canvas.y");
            int x = Integer.parseInt(rupProp.getProperty("canvas.x"));
            int y = Integer.parseInt(rupProp.getProperty("canvas.y"));
            pozice = new Point(x, y);
        }catch(Exception e)  {
            pozice = new Point(0, 0);
        }
        return pozice;
    }



//== SOUKROMÉ A POMOCNÉ METODY INSTANCÍ ========================================

    /***************************************************************************
     * Vrátí obrázek na aktivním plátně.
     * @return Obsah plátna jako obrázek
     */
    private BufferedImage getBufferedImage()
    {
        if (obrazPlátna instanceof BufferedImage) {
            return (BufferedImage) obrazPlátna;         //==========>
        }
        else {
            return getBufferedImage(0, 0, šířkaBodů, výškaBodů);
        }
    }


    /***************************************************************************
     * Vrátí obrázek výřezu na aktivním plátně.
     * @return Výřez obsahu plátna jako obrázek
     */
    private BufferedImage getBufferedImage(int x, int y, int šířka, int výška)
    {
        BufferedImage ret = new BufferedImage(šířka, výška,
                                              BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = (Graphics2D)ret.getGraphics();
        g2d.drawImage(obrazPlátna, -x, -y, Kreslítko.OBRAZOR);
        return ret;
    }


    /***************************************************************************
     * Připraví čáry vyznačující jednotlivá pole aktivního plátna.
     * Pokud se čáry kreslit nemají, vyprázdní odkazy na ně.
     */
    private void připravČáry()
    {
        if (mřížka  &&  (krok > 1))
        {
            if ((svislá == null)  ||  (svislá.length != sloupců)) {
                svislá = new Čára[sloupců];
            }
            if ((vodorovná == null)  ||  (vodorovná.length != řádků)) {
                vodorovná = new Čára[řádků];
            }
            for (int i=0, x=krok;   i < sloupců;      i++, x+=krok) {
                svislá[i] = new Čára(x, 0, x, výškaBodů, barvaČar);
            }
            for (int i=0, y=krok;   i < řádků;   i++, y+=krok) {
                vodorovná[i] = new Čára(0, y, šířkaBodů, y, barvaČar);
            }
        }
        else
        {
            //Uvolnění doposud používaných instancí
            svislá    = null;
            vodorovná = null;
            mřížka    = false;
        }
    }


    /***************************************************************************
     * Vytvoří a připraví okno, které pak bude správce používat.
     * Možná je implementace zbytečně rafinovaná,
     * ale před tím se to nechutně často kousalo.
     *
     * @return Připravené okno
     */
    private JFrame připravOkno()
    {
        final JFrame[] oo = new JFrame[1];
        Runnable r = new Runnable() {
            @Override public void run()
            {
                JFrame jfOkno = new JFrame(); //Vytvoří nové aplikační okno
                jfOkno.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                jfOkno.setTitle("POLOTOVAR");
                //Zavřením okna se zavře celá aplikace
                oo[0] = jfOkno;
            }
        };
        try {
            javax.swing.SwingUtilities.invokeAndWait(r);
        } catch (Exception ex) {
            String s = "Vytvoření okna se nepodařilo";
            System.err.println(s);
            JOptionPane.showMessageDialog(null, s);
            System.exit(1);
        }
//%I+ @DB_SP
//        System.out.println("Vytvoření okna se zdařilo (snad)");
//%I-

        return oo[0];
    }



//== VNOŘENÉ A VNITŘNÍ TŘÍDY ===================================================
//== TESTY A METODA MAIN =======================================================
//%A+ >135

    private static void w(){try{
        Thread.sleep(10);}catch(InterruptedException e){}
    }


    /***************************************************************************
     * Testovaci metoda
     */
    public static void test() {
        SP.přidej(new Obdélník());                    w();
        SP.přidej(new Elipsa(), new Trojúhelník());   w();
        IO.zpráva("Výchozí obrázek - budu vyjímat výřez");
        Obrázek obr = SP.getObrázek(50, 0, 75, 75);   w();
        SP.přidej(obr);                               w();
        SP.setBarvaPozadí(Barva.ČERNÁ);               w();
        IO.zpráva("Obrázek přidaný?");
        obr.setPozice(100, 50);                       w();
        IO.zpráva("Posunutý?");
        obr.setRozměr(150, 150);                        w();
        IO.zpráva("Zvětšený?");
        SP.setKrokRozměr(50, 5, 2);
//        SP.setKrokRozměr(1, 50, 50);
//        SP.uložJakoObrázek(new File("D:/SMAZAT.PNG"));

        System.exit(0);
    }


    /***************************************************************************
     * Testovaci metoda
     */
    public static void test2()
    {
        IO.zpráva("Hotovo");
        SP.přidej(new Obdélník   (0, 0, 300, 300));
        SP.přidej(new Elipsa     (0, 0, 300, 300));
        SP.přidej(new Trojúhelník(0, 0, 300, 300));
        IO.zpráva("Nakresleno");

        System.exit(0);
    }
    /** @param args Paremtry příkazového řádku - nepoužité  */
    public static void main(String[] args) { test2(); } /**/
//%A-
}
